{
 "cells": [
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# BentoML Kubeflow Notebook Example\n",
    "\n",
    "In this example, we will train three fraud detection models using the [Kaggle IEEE-CIS Fraud Detection dataset](https://www.kaggle.com/c/ieee-fraud-detection) using the Kubeflow notebook and create a BentoML service that simultaneously invoke all three models and returns the decision if any one of the models predicts that a transactin is a fraud. We will build and push the BentoML service to an S3 bucket. Next we will containerize BentoML service from the S3 bucket and deploy the service to Kubeflow cluster using using BentoML custom resource definitions on Kubernetes. The service will be deployed in a microservice architecture with each model running in a separate pod, deployed on hardware that is the most ideal for running the model, and scale independently."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Prerequisites\n",
    "\n",
    "This guide assume that Kubeflow is already installed in Kubernetes cluster. See [Kubeflow Manifests](https://github.com/kubeflow/manifests) for installation instructions.\n",
    "\n",
    "Install BentoML cloud native components and custom resource definitions."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! kustomize build bentoml-yatai-stack/default | kubectl apply -n kubeflow --server-side -f -"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Install the required packages to run this example."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! pip install -r requirements.txt"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Download Kaggle Dataset\n",
    "\n",
    "Set Kaggle username and key as environment variables. Accepting the [rules of the competition](https://www.kaggle.com/competitions/ieee-fraud-detection/rules) is required for downloading the dataset."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Set Kaggle Credentials for downloading dataset\n",
    "%env KAGGLE_USERNAME=\n",
    "%env KAGGLE_KEY="
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "!kaggle competitions download -c ieee-fraud-detection\n",
    "!rm -rf ./data/\n",
    "!unzip -d ./data/ ieee-fraud-detection.zip && rm ieee-fraud-detection.zip"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train Models\n",
    "\n",
    "In this demonstration, we'll train three fraud detection models using the Kaggle IEEE-CIS Fraud Detection dataset. To showcase saving and serving multiple models with Kubeflow and BentoML, we'll split the dataset into three equal-sized chunks and use each chunk to train a separate model. While this approach has no practical benefits, it will help illustrate how to save and serve multiple models with Kubeflow and BentoML."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "df_transactions = pd.read_csv(\"./data/train_transaction.csv\")\n",
    "\n",
    "X = df_transactions.drop(columns=[\"isFraud\"])\n",
    "y = df_transactions.isFraud"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.preprocessing import OrdinalEncoder\n",
    "\n",
    "numeric_features = df_transactions.select_dtypes(include=\"float64\").columns\n",
    "categorical_features = df_transactions.select_dtypes(include=\"object\").columns\n",
    "\n",
    "preprocessor = ColumnTransformer(\n",
    "    transformers=[\n",
    "        (\"num\", SimpleImputer(strategy=\"median\"), numeric_features),\n",
    "        (\n",
    "            \"cat\",\n",
    "            OrdinalEncoder(handle_unknown=\"use_encoded_value\", unknown_value=-1),\n",
    "            categorical_features,\n",
    "        ),\n",
    "    ],\n",
    "    verbose_feature_names_out=False,\n",
    "    remainder=\"passthrough\",\n",
    ")\n",
    "\n",
    "X = preprocessor.fit_transform(X)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Define our training function with the number of boosting rounds and maximum depths."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [],
   "source": [
    "import xgboost as xgb\n",
    "\n",
    "\n",
    "def train(n_estimators, max_depth, X_train, y_train, X_test, y_test):\n",
    "    return xgb.XGBClassifier(\n",
    "        tree_method=\"hist\",\n",
    "        n_estimators=n_estimators,\n",
    "        max_depth=max_depth,\n",
    "        eval_metric=\"aucpr\",\n",
    "        objective=\"binary:logistic\",\n",
    "        enable_categorical=True,\n",
    "    ).fit(X_train, y_train, eval_set=[(X_test, y_test)])"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We will divide the training data into three equal-sized chunks and treat them as independent data sets. Based on these data sets, we will train three separate fraud detection models. The trained model will be saved to the local model store using BentoML model saving API."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import bentoml\n",
    "\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "CHUNKS = 3\n",
    "CHUNK_SIZE = len(X) // CHUNKS\n",
    "\n",
    "for i in range(CHUNKS):\n",
    "    START = i * CHUNK_SIZE\n",
    "    END = (i + 1) * CHUNK_SIZE\n",
    "    X_train, X_test, y_train, y_test = train_test_split(X[START:END], y[START:END])\n",
    "\n",
    "    name = f\"ieee-fraud-detection-{i}\"\n",
    "    model = train(100, 5, X_train, y_train, X_test, y_test)\n",
    "    score = model.score(X_test, y_test)\n",
    "    print(f\"Successfully trained model {name} with score {score}.\")\n",
    "\n",
    "    bentoml.xgboost.save_model(\n",
    "        name,\n",
    "        model,\n",
    "        signatures={\n",
    "            \"predict_proba\": {\"batchable\": True},\n",
    "        },\n",
    "        custom_objects={\"preprocessor\": preprocessor},\n",
    "    )\n",
    "    print(f\"Successfully saved model {name} to the local model store.\")"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Saved models can be loaded back into the memory and debugged in the notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import bentoml\n",
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "model_ref = bentoml.xgboost.get(\"ieee-fraud-detection-0:latest\")\n",
    "model_runner = model_ref.to_runner()\n",
    "model_runner.init_local()\n",
    "model_preprocessor = model_ref.custom_objects[\"preprocessor\"]\n",
    "\n",
    "test_transactions = pd.read_csv(\"./data/test_transaction.csv\")[0:500]\n",
    "test_transactions = model_preprocessor.transform(test_transactions)\n",
    "result = model_runner.predict_proba.run(test_transactions)\n",
    "np.argmax(result, axis=1)"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Define Service API\n",
    "\n",
    "After the models are built and scored, let's create the service definition. You can find the service definition in the `service.py` module in this example. Let's breakdown the `service.py` module and explain what each section does.\n",
    "\n",
    "First, we will create a list of preprocessors and runners from the three models we saved earlier. Runners are abstractions of the model inferences that can be scaled independently. See [Using Runners](https://docs.bentoml.com/en/latest/concepts/runner.html) for more details.\n",
    "\n",
    "```python\n",
    "fraud_detection_preprocessors = []\n",
    "fraud_detection_runners = []\n",
    "\n",
    "for model_name in [\"ieee-fraud-detection-0\", \"ieee-fraud-detection-1\", \"ieee-fraud-detection-2\"]:\n",
    "    model_ref = bentoml.xgboost.get(model_name)\n",
    "    fraud_detection_preprocessors.append(model_ref.custom_objects[\"preprocessor\"])\n",
    "    fraud_detection_runners.append(model_ref.to_runner())\n",
    "```\n",
    "\n",
    "Next, we will create a service with the list of runners passed in.\n",
    "\n",
    "```python\n",
    "svc = bentoml.Service(\"fraud_detection\", runners=fraud_detection_runners)\n",
    "```\n",
    "\n",
    "Finally, we will create the API function `is_fraud`. We'll use the `@api` decorator to declare that the function is an API and specify the input and output types as pandas.DataFrame and JSON, respectively. The function is defined as `async` so that the inference calls to the runners can happen simultaneously without waiting for the results to return before calling the next runner. The inner function `_is_fraud` defines the model inference logic for each runner. All runners are called simultaneously through the `asyncio.gather` function and the results are aggregated into a list. The function will return True if any of the models return True.\n",
    "\n",
    "For more about service definitinos, please see [Service and APIs](https://docs.bentoml.com/en/latest/concepts/service.html)."
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Build Service\n",
    "\n",
    "Building the service and models into a bento allows it to be distributed among collaborators, containerized into a OCI image, and deployed in the Kubernetes cluster. To build a service into a bento, we first need to define the `bentofile.yaml` file. See [Building Bentos](https://docs.bentoml.com/en/latest/concepts/bento.html) for more options.\n",
    "\n",
    "```yaml\n",
    "service: \"service:svc\"\n",
    "include:\n",
    "- \"service.py\"\n",
    "- \"sample.py\"\n",
    "python:\n",
    "  requirements_txt: ./requirements.txt\n",
    "```\n",
    "\n",
    "Running the following command will build the service into a bento and store it to the local bento store."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! bentoml build"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Serve Bento\n",
    "\n",
    "Serving the bento will bring up a service endpoint in HTTP or gRPC for the service API we defined. Use `--help` to see more serving options."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "! bentoml serve-http --production"
   ]
  },
  {
   "attachments": {},
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Deploy to Kubernetes Cluster\n",
    "\n",
    "Great work! You have successfully built and tested the Fraud Detection Bento. Next, we will deploy the bento to the Kubernetes cluster. Proceed to the README of the example for the next steps."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "bentoml",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.8"
  },
  "orig_nbformat": 4,
  "vscode": {
   "interpreter": {
    "hash": "c7496e3357ac7d0feefccc058dccef5f5223d152b10dff998de4314227246f61"
   }
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
